/** \file gu_shader.cpp */

#include "gu_shader.h"
#include "gu_util.h"
#include "gu_shader_parser.h"

#include <gr_algorithms.h>

#include <GL/glew.h>

#include<algorithm>
#include<cassert>

namespace gu
{

class GlShader : public Shader
{
    VariableMap variableMap_;
    ShaderDialect dialect_;
public:
    GlShader():program_(0), dialect_(GLSL_330){}

    ~GlShader()
    {
        finalize();
    }

    void setSource(ShaderType t, std::string& str) override
    {
        setSource(t, str.c_str());
    }

    void setSource(ShaderType t, const char* str) override
    {
         source_[t] = str;
    }

    const std::string* getSource(ShaderType t) override
    {
        return gr::tryGetValue(source_, t);
    }

    uint32_t getUniformLocation(const char* str) override
    {
        uint32_t location = 0;
        if(program_)
        {
            location = glGetUniformLocation(program_, str);
            checkGlError("glGetUniformLocation");
        }
        return location;
    }

    uint32_t getAttribLocation(const char* str) override
    {
        uint32_t location = 0;
        if(program_)
        {
            location = glGetAttribLocation(program_, str);
            checkGlError("glGetAttribLocation");
        }
        return location;
    }

    uint32_t program() override {return program_;}

    void parseVariables()
    {
        ShaderType types[] = {GEOMETRY, FRAGMENT, VERTEX};
        auto getMap = [&](ShaderType& t){
            if(source_.count(t) > 0)
            {
                auto map = parseToVariableMap(dialect_, source_[t].c_str());
                variableMap_.insert(map.begin(), map.end());
            }
        };

        std::for_each(types, types + 3, getMap);
    }

    bool compile() override
    {
        bool result = true;
        if(gr::hasKey(source_, FRAGMENT) && gr::hasKey(source_, VERTEX))
        {
            GLenum errorCheckValue = glGetError();

            auto compileShader = [&] (GLenum shaderType, GLuint& handle, const char* source)
            {
                handle = glCreateShader(shaderType);
                glShaderSource(handle, 1, &source, NULL);
                glCompileShader(handle);
            };

            compileShader(GL_VERTEX_SHADER, vertexShader_, source_[VERTEX].c_str());
            compileShader(GL_FRAGMENT_SHADER, fragmentShader_, source_[FRAGMENT].c_str());

            program_ = glCreateProgram();
            glAttachShader(program_, vertexShader_);
            glAttachShader(program_, fragmentShader_);
            glLinkProgram(program_);
            glUseProgram(program_);

            // TODO: add output of compile error messages to log
            // TODO: implement log!

            result = guCheckGlError("GlShader::compile");
        }

        if(result)
        {
            parseVariables();
        }

        return result;
    }

    ShaderDialect dialect() override {return dialect_;}

    VariableMap& variableMap() override{return variableMap_;}

    bool finalize()
    {
        bool result = true;
        if(program_)
        {
            GLenum errorCheckValue = glGetError();

            glUseProgram(0);

            glDetachShader(program_, vertexShader_);
            glDetachShader(program_, fragmentShader_);

            glDeleteShader(fragmentShader_);
            glDeleteShader(vertexShader_);

            glDeleteProgram(program_);

            errorCheckValue = glGetError();
            if (errorCheckValue != GL_NO_ERROR)
            {
                result = false;
            }
            result = guCheckGlError("GlShader::finalize");
        }
        return result;
    }

private:
    GLuint program_;
    GLuint fragmentShader_;
    GLuint vertexShader_;
    std::map<ShaderType, std::string> source_;
};

Shader* newShader(ShaderDialect dialect)
{
    switch(dialect)
    {
        case GLSL_330: return new GlShader(); break;
        default:assert(!"Undefined enum value");
    }
    return nullptr;
}

Shader* newShader(ShaderDialect dialect, const char* vertexShader, const char* fragmentShader)
{
    Shader* shader = newShader(dialect);
    shader->setSource(FRAGMENT, fragmentShader);
    shader->setSource(VERTEX, vertexShader);

    if(! shader->compile())
    {
        delete shader;
        shader = nullptr;
        assert(!"Cannot compile shader");
    }

    return shader;
}


void unuseShader()
{
    glUseProgram(0);
}

void useShader(Shader* shader)
{
    glUseProgram(shader->program());
}

}
